import torch
import torch.nn as nn

from codesecurity.tasks.malicious_code_detect.meta import SuperParameter_MCD


def get_model(sp:SuperParameter_MCD,model_name,device):
    
    if model_name is None:
        return ComplexModel(sp,device)
    
    model_name=model_name.lower()
    
    if model_name=='resnet':
        return ResNetModel(sp,device)
    elif model_name=='cnn':
        return CNNModel(sp,device)
    elif model_name=='attention' or model_name=='att':
        return AttentionModel(sp,device)
    elif model_name=='complex':
        return ComplexModel(sp,device)
    elif model_name=='segra':
        return SeGraModel(sp,device)
    elif model_name=='segra_ast':
        return SeGraModel_ast(sp,device)
    elif model_name=='segra_dfg':   
        return SeGraModel_dfg(sp,device)
    else:
        return ResNetModel(sp,device)
    


class CNNModel(nn.Module):
    def __init__(self,channel,path_number,step_number,device,class_number=2,input_type='bddc'):
        super(CNNModel, self).__init__()
        self.conv1 = nn.Conv2d(channel, channel*2, kernel_size=3, stride=1, padding=1,device=device)
        self.relu = nn.ReLU()
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
        self.conv2 = nn.Conv2d(channel*2, channel*4, kernel_size=3, stride=1, padding=1,device=device)
        self.fc = nn.Linear(channel*4 * path_number//4 * step_number//4, class_number,device=device)

        self.device=device
        self.input_type=input_type

    def forward(self, x):
        
        x=x.to(torch.float32)
        x=x.to(self.device)
        if self.input_type=='bddc':
            x=torch.permute(x,(0,3,1,2))
        
        x = self.conv1(x)
        x = self.relu(x)
        x = self.pool(x)
        x = self.conv2(x)
        x = self.relu(x)
        x = self.pool(x)
        x = torch.flatten(x, 1)
        
        x=torch.dropout(x,0.5,self.training)
        
        x = self.fc(x)
        return x  

class AttentionModel(nn.Module):
    def __init__(self,input_channel,step_number,path_number,device,class_number=2) -> None:
        super(AttentionModel, self).__init__()
        self.device=device
        #self.sp=sp

        self.linear=nn.Linear(input_channel,32,device=device)
        self.att=SelfAttention(32,device=device)
        self.step_number=step_number
        
        
        self.conv1 = nn.Conv2d(1, 8, kernel_size=3, stride=1, padding=1,device=device)
        self.conv1_1 = nn.Conv2d(8, 8, kernel_size=3, stride=1, padding=1,device=device)
        self.relu = nn.ReLU()
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
        self.conv2 = nn.Conv2d(8, 16, kernel_size=3, stride=1, padding=1,device=device)
        self.conv2_1=nn.Conv2d(16, 16, kernel_size=3, stride=1, padding=1,device=device)
        self.classifier=nn.Linear(2 * 16 * path_number,class_number,device=device)
        
    def forward(self,x):
        x=x.to(torch.float32)
        x=x.to(self.device)

        path_numbr=x.shape[1]
        
        #print(x.shape)
        
        x=x.view(x.shape[0]*x.shape[1],x.shape[2],-1)
        
        x=self.linear(x)
        x=torch.relu(x)
        x=self.att(x)
        x=x.sum(1)
        x=x.view(-1,1,path_numbr,32)
        
        x = self.conv1(x)
        x = self.relu(x)
        x = self.conv1_1(x)
        x = self.relu(x)
        x = self.pool(x)
        x = self.conv2(x)
        x = self.relu(x)
        x = self.conv2_1(x)
        x = self.relu(x)
        x = self.pool(x)
        #x = torch.sum(x,1)
        x = torch.flatten(x,1)
        
        #x=torch.dropout(x,0.5,self.training)
        
        x = self.classifier(x)
        return x  

class SelfAttention(nn.Module):
    def __init__(self, hidden_dim,device):
        super(SelfAttention, self).__init__()
        self.hidden_dim = hidden_dim
        self.query = nn.Linear(hidden_dim, hidden_dim,device=device)
        self.key = nn.Linear(hidden_dim, hidden_dim,device=device)
        self.value = nn.Linear(hidden_dim, hidden_dim,device=device)
        self.softmax = nn.Softmax(dim=2)
        self.device=device

    def forward(self, inputs):
        query = self.query(inputs)
        key = self.key(inputs)
        value = self.value(inputs)
    
        attention_scores = torch.matmul(query, key.transpose(1, 2))
        attention_weights = self.softmax(attention_scores)

        attention_output = torch.matmul(attention_weights, value)
        return attention_output    

class ResNetModel(nn.Module):
    def __init__(self,sp:SuperParameter_MCD,device,class_number=2,input_type='bddc'):
        super(ResNetModel, self).__init__()
        
        self.resnet=ResNet(sp,device,num_classes=class_number)
        
        self.device=device
        self.input_type=input_type

    def forward(self, x):
        
        x=x.to(torch.float32)
        x=x.to(self.device)
        if self.input_type=='bddc':
            x=torch.permute(x,(0,3,1,2))
        
        return self.resnet(x)

class ResidualBlock(nn.Module):
    expansion = 1
    def __init__(self, in_channels, out_channels,device, stride=1, downsample=None):
        super(ResidualBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False,device=device)
        self.bn1 = nn.BatchNorm2d(out_channels,device=device)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False,device=device)
        self.bn2 = nn.BatchNorm2d(out_channels,device=device)
        self.downsample = downsample
        self.device=device

    def forward(self, x):
        identity = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        if self.downsample is not None:
            identity = self.downsample(x)

        out += identity
        out = self.relu(out)

        return out

class ResNet(nn.Module):
    def __init__(self,channel,device, block=ResidualBlock, layers=[2,2,2,2], num_classes=2):
        super(ResNet, self).__init__()
        self.in_channels = channel*2
        self.conv1 = nn.Conv2d(channel, channel*2, kernel_size=3, stride=1, padding=1, bias=False,device=device)
        self.bn1 = nn.BatchNorm2d(channel*2,device=device)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.device=device

        self.layer1 = self._make_layer(block, self.in_channels, layers[0])
        self.layer2 = self._make_layer(block, self.in_channels*2, layers[1])
        self.layer3 = self._make_layer(block, self.in_channels*4, layers[2])

        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(self.in_channels * block.expansion, num_classes,device=device)
        
    def _make_layer(self, block, out_channels, blocks, stride=1):
        downsample = None
        if stride != 1 or self.in_channels != out_channels * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channels, out_channels * block.expansion, kernel_size=1, stride=stride, bias=False,device=self.device),
                nn.BatchNorm2d(out_channels * block.expansion,device=self.device),
            )

        layers = []
        layers.append(block(self.in_channels, out_channels,self.device, stride, downsample))
        self.in_channels = out_channels * block.expansion
        for _ in range(1, blocks):
            layers.append(block(self.in_channels, out_channels,self.device))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x


class SeGraModel(nn.Module):
    def __init__(self,sp:SuperParameter_MCD,device,class_number=2) -> None:
        super(SeGraModel, self).__init__()
        self.device=device
        self.sp=sp

        self.hidden_dim=64

        self.ast_extractor=SeGraModel_ast(sp,device,-1)
        self.dfg_extractor=SeGraModel_dfg(sp,device,-1)
  
        self.classifier=nn.Linear(self.sp.ast_feature_hidden_dim+self.sp.dfg_feature_hidden_dim,class_number,device=device)
        
    def forward(self,dfg,ast):
        dfg=self.dfg_extractor(dfg)
        ast=self.ast_extractor(ast)
        
        x=torch.cat([dfg,ast],1)
        x=torch.relu(x)
        
        x = self.classifier(x)
        return x


class SeGraModel_dfg(nn.Module):
    def __init__(self,sp:SuperParameter_MCD,device,class_number=2) -> None:
        super(SeGraModel_dfg, self).__init__()
        self.device=device
        self.sp=sp

        self.hidden_dim=64

        self.class_number=class_number

        self.linear_dfg=nn.Linear(self.sp.ast_node_dim,self.hidden_dim,device=device)

        self.dfg_extractor=AttentionModel(self.hidden_dim,sp.step_number,sp.path_number*sp.channel,device,sp.dfg_feature_hidden_dim)
        
        if self.class_number>1:
            self.classifier=nn.Linear(self.sp.dfg_feature_hidden_dim,class_number,device=device)

    def forward(self,dfg):
        dfg=dfg.to(torch.float32)
        
        dfg=dfg.to(self.device)

        dfg=self.linear_dfg(dfg)
        dfg=torch.relu(dfg)

        dfg=torch.reshape(dfg,(dfg.shape[0],self.sp.path_number*self.sp.channel,self.sp.step_number,self.hidden_dim))
        dfg=self.dfg_extractor(dfg)
        
        dfg=torch.dropout(dfg,0.5,self.training)
        
        if self.class_number>1:
            dfg = self.classifier(dfg)

        return dfg

class SeGraModel_ast(nn.Module):
    def __init__(self,sp:SuperParameter_MCD,device,class_number=2) -> None:
        super(SeGraModel_ast, self).__init__()
        self.device=device
        self.sp=sp

        self.hidden_dim=64

        self.linear_ast=nn.Linear(self.sp.ast_node_dim,self.hidden_dim,device=device)

        self.ast_extractor=AttentionModel(self.hidden_dim,sp.ast_path_max_length,sp.path_number,device,sp.ast_feature_hidden_dim)
        
        self.class_number=class_number

        if self.class_number>1:
            self.classifier=nn.Linear(self.sp.ast_feature_hidden_dim,class_number,device=device)

    def forward(self,ast):
        ast=ast.to(torch.float32)
        
        ast=ast.to(self.device)

        ast=self.linear_ast(ast)
        ast=torch.relu(ast)

        ast=torch.reshape(ast,(ast.shape[0],self.sp.path_number,self.sp.ast_path_max_length,self.hidden_dim))
        ast=self.ast_extractor(ast)
        
        ast=torch.dropout(ast,0.5,self.training)
        
        if self.class_number>1:
            ast = self.classifier(ast)

        return ast


class ComplexModel(nn.Module):
    def __init__(self,sp:SuperParameter_MCD,device,class_number=2,input_type='bddc'):
        super(ComplexModel, self).__init__()
        
        self.resnet=ResNetModel(sp,device,class_number=sp.dfg_feature_hidden_dim,input_type=input_type)
        self.attention=AttentionModel(sp.ast_node_dim,sp.path_number,device,class_number=sp.ast_feature_hidden_dim)
        self.device=device
        self.input_type=input_type
        
        self.classifier=nn.Linear(sp.dfg_feature_hidden_dim+sp.ast_feature_hidden_dim,class_number,device=device)

    def forward(self, x1,x2):
        x1=self.resnet(x1)
        
        x2=self.attention(x2)
        
        x=torch.cat([x1,x2],1)
        
        x=torch.dropout(x,0.5,self.training)
        
        return self.classifier(x)
# class BasicModel(torch.nn.Module):
#     def __init__(self,sp:SuperParameter_MCD,device) -> None:
#         super().__init__()
        
#         self.lexical_input_dim=sp.word_bag_dim
#         self.syntactic_input_dim=sp.word_bag_dim
        
#         self.syntactic_hidden_dim=sp.syntactic_vec_dim
#         self.lexical_hidden_dim=sp.lexical_vec_dim
#         self.channel=sp.channel
        
#         self.device=device
        
        
        
#         self.linear_layer_lexical=torch.nn.Linear(self.lexical_input_dim,self.lexical_hidden_dim,device=device)
#         self.linear_layer_syntactic=torch.nn.Linear(self.syntactic_input_dim,self.syntactic_hidden_dim,device=device)
        
#         self.encoder=torch.nn.Sequential(
#             torch.nn.Linear(1,self.channel,device=self.device),
#             torch.nn.ReLU(),
#             torch.nn.Linear(self.channel,self.channel,device=self.device)            
#         )

#         self.classfier=torch.nn.Linear(self.lexical_hidden_dim+self.syntactic_hidden_dim,2,device=device)

#     def forward(self,x1:torch.Tensor,x2:torch.Tensor):
        
#         assert len(x1.shape)==2,len(x2.shape)==2
        
#         x1=x1.to(self.device)
#         x2=x2.to(self.device)
        
#         x1=self.linear_layer_lexical(x1)
#         x1=torch.relu(x1)
        
#         x2=self.linear_layer_syntactic(x2)
#         x2=torch.relu(x2)
        
#         x=torch.concat([x1,x2],1)
        
#         x=x.reshape(x.shape[0],x.shape[1],1)
        
#         x=torch.dropout(x,0.5,self.training)
        
#         x=self.encoder(x)
#         x=torch.relu(x)

#         x=x.sum(2)
        
#         return self.classfier(x)